/*******************************************************************************
 * Copyright (c) 2007, Geophysical Center RAS
 * Sergey Berezin, Dmitry Voitsekhovskiy
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * - Neither the name of the Geophysical Center RAS nor the names
 * of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
using System;
using System.Xml;
using System.IO;
using System.Diagnostics;
using System.Configuration;
using System.Collections.Generic;
using System.Globalization;

using DAI.ClientToolkit;
using VisualEsse.DataEngine.Types;
using VisualEsse.DataEngine;

// TODO: add an extra class or methods in EsseResponse to return data as .NET typed arrays.

namespace VisualEsse
{
    /// <summary>This class represents one of the parameters in ESSE request</summary>
    public struct EsseParameter
    {
        string name;
        int level;

        public EsseParameter(string name, int level)
        {
            this.name = name;
            this.level = level;
        }

        public EsseParameter(string s)
        {
            int idx = s.IndexOf('-');
            name = s.Substring(0, idx);
            level = Int32.Parse(s.Substring(idx + 1));
        }

        public string Name
        {
            get
            {
                return name;
            }
        }

        public string FullName
        {
            get
            {
                if (level == 0)
                    return name + ".surface";
                else
                    return name + ".pressure_level";
            }
        }

        public int Level
        {
            get
            {
                return level;
            }
        }

        public override string ToString()
        {
            return String.Format("{0}-{1}", name, level);
        }

        public override bool Equals(object obj)
        {
            if (obj is EsseParameter)
            {
                EsseParameter p2 = (EsseParameter)obj;
                return this == p2;
            }
            else
                return false;
        }

        public override int GetHashCode()
        {
            return name.GetHashCode() ^ level.GetHashCode();
        }

        public static bool operator ==(EsseParameter p1, EsseParameter p2)
        {
            return (p1.name == p2.name && p1.level == p2.level);
        }

        public static bool operator !=(EsseParameter p1, EsseParameter p2)
        {
            return (p1.name != p2.name || p1.level != p2.level);
        }
    }

    public delegate void EsseRegionChangedHandler(EsseRegion drr);

    /// <summary>Represents spatial and temporal region of ESSE request</summary>
    public class EsseRegion : ICloneable
    {
        protected List<double2> points = new List<double2>();

        protected DateTime startDate;
        protected DateTime stopDate;

        /// <summary>Step between data in minutes</summary>
        protected int timeStep;

        protected double latStep = -1;
        protected double lonStep = -1;

        public double LatStep
        {
            get { return latStep; }
        }

        public double LonStep
        {
            get { return lonStep; }
        }

        public EsseRegionChangedHandler RegionChanged;

        protected virtual void OnRegionChanged()
        {
            if (RegionChanged != null)
                RegionChanged(this);
        }

        public DateTime From
        {
            get { return startDate; }
            set
            {
                startDate = value;
                OnRegionChanged();
            }
        }

        public DateTime To
        {
            get { return stopDate; }
            set
            {
                stopDate = value;
                OnRegionChanged();
            }
        }

        /// <summary>Gets or sets step size in minute</summary>
        public TimeSpan TimeStep
        {
            get { return new TimeSpan(0, timeStep, 0); }
            set
            {
                timeStep = (int)value.TotalMinutes;
                OnRegionChanged();
            }
        }

        public IEnumerable<double2> Points
        {
            get
            {
                return points;
            }
        }

        public int PointsCount
        {
            get
            {
                return points.Count;
            }
        }

        public void AddPoint(double2 pt)
        {
            points.Add(pt);

            int i = points.Count - 1;
            if (i > 0)
            {
                if (latStep <= 0)
                {
                    double2 prev = points[i - 1];
                    if (prev.y != pt.y) latStep = Math.Abs(pt.y - prev.y);
                }

                if (lonStep <= 0)
                {
                    double2 prev = points[i - 1];
                    if (prev.x != pt.x) lonStep = Math.Abs(pt.x - prev.x);
                }
            }

            OnRegionChanged();
        }

        public void RemovePoint(double2 pt)
        {
            points.Remove(pt);
            OnRegionChanged();
        }

        public double2 GridMin
        {
            get
            {
                double2 min = points[0];
                for (int i = 1; i < points.Count; i++)
                    min = double2.Min(min, points[i]);
                return min;
            }
        }

        public double2 GridMax
        {
            get
            {
                double2 max = points[0];
                for (int i = 1; i < points.Count; i++)
                    max = double2.Max(max, points[i]);
                return max;
            }
        }


        public object Clone()
        {
            EsseRegion r2 = new EsseRegion();
            r2.startDate = startDate;
            r2.stopDate = stopDate;
            r2.timeStep = timeStep;
            List<double2> points2 = new List<double2>(points.Count);
            foreach (double2 pt in points)
                points2.Add(pt);
            r2.points = points2;
            return r2;
        }
    }

    /// <summary>Hold all data necessary to perform ESSE request</summary>
    public class EsseRequest
    {
        protected string serviceURL;
        protected string resourceName;

        protected EsseRegion region;

        protected List<EsseParameterDescription> parameters = new List<EsseParameterDescription>();

        /// <summary>When true dumps every response to file</summary>
        protected bool dumpCompressed;

        public EsseRequest()
        {
            AppSettingsReader apr = new AppSettingsReader();
            try
            {
                serviceURL = VisualEsse.DataEngine.Properties.Settings.Default.DataServiceUrl;
            }
            catch (Exception exc)
            {
                serviceURL = "http://teos1.wdcb.ru/axis/services/ogsadai/EsseService";
                Trace.WriteLine(exc.Message, "EsseRequest");
            }
            try
            {
                resourceName = (string)apr.GetValue("EsseRequest.resourceName", typeof(string));
            }
            catch (Exception exc)
            {
                resourceName = "EsseResource";
                Trace.WriteLine(exc.Message, "EsseRequest");
            }
            try
            {
                dumpCompressed = (bool)apr.GetValue("EsseRequest.DumpCompressedResponse", typeof(bool));
            }
            catch /*(Exception exc)*/
            {
                dumpCompressed = false;
            }
            region = new EsseRegion();
        }

        public EsseRegion Region
        {
            get
            {
                return region;
            }
        }

        public IEnumerable<EsseParameterDescription> Parameters
        {
            get
            {
                return parameters;
            }
        }

        public void AddParameter(EsseParameterDescription p)
        {
            parameters.Add(p);
        }

        public void RemoveParameter(EsseParameterDescription p)
        {
            parameters.Remove(p);
        }

        public XmlElement PerformXml()
        {
            DataService dai = new DataService(serviceURL);
            Trace.WriteLine(String.Format("DAI service {0}\nVersion:{1}\nEsse Data Resource Service {0}\n", serviceURL, dai.Version, resourceName));
            DataServiceResource esse = dai.DataServiceResource(resourceName);
            Trace.WriteLine("Supported activities:\n" + string.Join("\n", esse.Activities));

            if (parameters.Count == 0)
                throw new InvalidOperationException("Cannot perform request without parameters");

            if (parameters.Count == 0)
                throw new InvalidOperationException("Cannot perform request without specified points");

            // List of activities
            object[] activities = new object[2 * parameters.Count];
            int currentActivity = 0;

            foreach (EsseParameterDescription parameter in parameters)
            {
                string paramStr = parameter.EsseParameter.ToString();

                // Prepare first activity - data request
                object[] ap = new object[5 + region.PointsCount];
                ap[0] = new GetXmlDataActivityTypeOutput("XmlData" + paramStr);
                ap[1] = new GetXmlDataActivityTypeValue("source", "ncep25");
                string fromString = region.From.Year.ToString() + "-" +
                    region.From.Month.ToString() + "-" +
                    region.From.Day.ToString();
                string toString = region.To.Year.ToString() + "-" +
                    region.To.Month.ToString() + "-" +
                    region.To.Day.ToString();
                int stepMinutes = (int)region.TimeStep.TotalMinutes;
                ap[2] = new GetXmlDataActivityTypeRange("time", fromString, toString, stepMinutes.ToString(CultureInfo.InvariantCulture));
                ap[3] = new GetXmlDataActivityTypeValue("parameter", parameter.EsseParameter.FullName);
                ap[4] = new GetXmlDataActivityTypeValue("level", parameter.EsseParameter.Level.ToString(CultureInfo.InvariantCulture));
                int iap = 5;

                foreach (double2 pt in region.Points)
                {
                    ap[iap++] = new GetXmlDataActivityTypeValue("point",
                            string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:f1} {1:f1}",
                                            pt.x, pt.y));
                }
                activities[currentActivity++] = new GetXmlDataActivityType("DataToXml" + paramStr, ap);

                // Prepare second activity - send compressed data
                activities[currentActivity++] = new GZIPCompressionType("gzip" + paramStr, "XmlData" + paramStr, "XmlDataCompressed" + paramStr, GZIPCompressionTypeSettingsChecksum.adler32);
            }
            Trace.WriteLine("Activity list prepared. Starting service call...\n");
            XmlElement responseElement = esse.Perform(new PerformType(null, null, activities));

            if (dumpCompressed)
            {
                StreamWriter traceOutput = new StreamWriter(GetDumpFileName("EsseResponseCompressed", "xml"));
                traceOutput.Write(responseElement.OuterXml);
                traceOutput.Close();
            }

            return responseElement;
        }

        public EsseResponse Perform()
        {
            return new EsseResponse(this, PerformXml());
        }

        public void StoreXml(string path)
        {
            XmlElement result = PerformXml();
            StreamWriter sw = new StreamWriter(path);
            sw.WriteLine("<?xml version=\"1.0\"?>");
            sw.WriteLine(PerformXml().OuterXml);
            sw.Close();
        }

        public static string GetDumpFileName(string prefix, string ext)
        {
            DateTime now = DateTime.Now;
            return prefix + "_" + now.Hour + "_" + now.Minute + "_" + now.Second + "." + ext;
        }

    }

    public struct EsseResponseParameter
    {
        EsseRegion region;
        EsseParameterDescription parameter;
        EsseResponse response;

        public EsseResponseParameter(EsseParameterDescription parameter, EsseRegion region)
        {
            this.parameter = parameter;
            this.region = region;
            this.response = null;
        }

        public EsseResponseParameter(string s, EsseResponse response)
        {
            this.parameter = new EsseParameterDescription(new EsseParameter(s));
            this.region = null;
            this.response = response;
        }

        public EsseResponseParameter(EsseParameterDescription parameter, EsseResponse response)
        {
            this.parameter = parameter;
            this.region = null;
            this.response = response;
        }

        public EsseParameterDescription Parameter
        {
            get
            {
                return parameter;
            }
        }

        public EsseRegion Region
        {
            get
            {
                if (region == null)
                    if (response != null)
                        region = response.GetRegion(parameter);
                    else
                        throw new Exception("Region and response cannot both be null!");
                return region;
            }
        }
    }

    public class EsseResponse
    {
        protected bool isOGSADAIWrapped = false;
        protected List<EsseResponseParameter> parameters = new List<EsseResponseParameter>();
        protected XmlElement response;
        XmlNamespaceManager ns = new XmlNamespaceManager(new NameTable());

        public enum DataSource { NCEP, SST, ERA, Default };
        private DataSource dataSource;

        public DataSource Source
        {
            get { return dataSource; }
        }

        static protected bool dumpUncompressed;

        protected EsseResponse()
        {
            ns.AddNamespace("types", Namespaces.Types);
        }

        static EsseResponse()
        {
            try
            {
                AppSettingsReader apr = new AppSettingsReader();
                dumpUncompressed = (bool)apr.GetValue("EsseRequest.DumpUncompressedResponse", typeof(bool));
            }
            catch (Exception /*exc*/)
            {
                dumpUncompressed = false;
            }
        }

        public List<EsseResponseParameter> Parameters
        {
            get
            {
                return parameters;
            }
        }

        public EsseResponse(EsseRequest request, XmlElement response)
            : this()
        {
            this.response = response;
            EsseRegion r2 = (EsseRegion)request.Region.Clone();
            foreach (EsseParameterDescription p in request.Parameters)
            {
                EsseResponseParameter p2 = new EsseResponseParameter(p, r2);
                parameters.Add(p2);
                if (dumpUncompressed)
                {
                    StreamReader sr = new StreamReader(GetUncompressedStream(p));
                    StreamWriter traceOutput = new StreamWriter(EsseRequest.GetDumpFileName("EsseResponse_" + p.ToString(), "xml"));
                    traceOutput.Write(sr.ReadToEnd());
                    traceOutput.Close();
                }
            }
        }

        public EsseResponse(XmlReader reader)
            : this()
        {
            XmlDocument xmldoc = new XmlDocument();
            xmldoc.Load(reader);
            response = xmldoc.DocumentElement;
            XmlReader reader2 = new XmlNodeReader(response);

            isOGSADAIWrapped = false;

            //////////////////////////////////////
            // Trying to read an OGSA-DAI wrap
            while (reader2.ReadToFollowing("result",Namespaces.Types))
            {
                isOGSADAIWrapped = true;
                this.dataSource = DataSource.Default;

                try
                {
                    reader2.MoveToAttribute("name");
                    if (reader2.ReadAttributeValue())
                    {
                        const string prefix = "XmlDataCompressed";
                        const string suffix = "Metadata";
                        string paramName = reader2.Value;
                        if (paramName.StartsWith(prefix) && !paramName.EndsWith(suffix))
                        {
                            paramName = paramName.Substring(prefix.Length);
                            parameters.Add(new EsseResponseParameter(paramName, this));
                        }
                    }                    
                }
                catch (Exception exc)
                {
                    Trace.WriteLine("Failed to parse result element: " + exc.Message);
                }
            }

            ///////////
            // This document is not wrapped
            if (!isOGSADAIWrapped)
            {
                reader2 = new XmlNodeReader(response);                
                while(reader2.ReadToFollowing("DataParameter"))
                {
                    string localKey = "unknown-0";
                    string pname = null;
                    string sourceName = null;
                    string display = null;
                    string units = null;
                    PaletteDescription? palette = null;
                    
                    string verticalCoord = "";
                    int level = 0;

                    while (reader2.ReadToFollowing("value"))
                    {
                        string name = reader2.GetAttribute("name");
                        string value = reader2.GetAttribute("value");
                        
                        switch (name)
                        { 
                            case "source":
                                sourceName = value;
                                value = value.ToLowerInvariant();
                                if (value.StartsWith("ncep"))
                                    dataSource = DataSource.NCEP;
                                else if (value.StartsWith("sst"))
                                    dataSource = DataSource.SST;
                                else if (value.StartsWith("era"))
                                    dataSource = DataSource.ERA;
                                else
                                    dataSource = DataSource.Default;
                                break;

                            case "name":
                                pname = value;
                                break;

                            case "parameter":
                                localKey = value;
                                break;
                            case "level":
                                localKey += "-" + value;
                                if (!int.TryParse(value, out level))
                                {
                                    Debug.WriteLine("Cannot parse level value for parameter. Zero is taken.");
                                    level = 0;
                                }
                                break;

                            case "display":
                                display = value;
                                break;

                            case "units":
                                units = value;
                                break;

                            case "verticalCoord":
                                verticalCoord = value;
                                break;
                        }
                    }

                    EsseParameter esseParameter = new EsseParameter(localKey);
                    EsseParameterDescription parameter = new EsseParameterDescription(esseParameter);

                    bool goOnline = (display == null || units == null || palette == null);                   

                    if (goOnline)
                    {
                        try
                        {
                            EsseParameterDescription ep;
                            EsseMetadataProvider mprovider = EsseMetadataProvider.CreateEsseMetadataProvider(
                                DataEngineBase.HomeDirectory,
                                VisualEsse.DataEngine.Properties.Settings.Default.DataServiceUrl, 
                                VisualEsse.DataEngine.Properties.Settings.Default.ResourceName,
                                (sourceName == null) ? VisualEsse.DataEngine.Properties.Settings.Default.DataSource : sourceName);
                            if (pname != null)
                                ep = mprovider.GetDescription(pname, verticalCoord, level);
                            else
                                ep = mprovider.GetDescription(esseParameter);

                            parameter.Description = ep.Description;
                            parameter.Units = ep.Units;
                            parameter.PaletteDescription = ep.PaletteDescription;
                            parameter.VerticalCoord = ep.VerticalCoord;

                            verticalCoord = ""; // TODO: it may be (in further) included in file metadata

                            Debug.WriteLine("Extra metadata has been used to extend file metadata.");
                        }
                        catch (Exception ex)
                        {
                            goOnline = false;
                            Debug.WriteLine("Extra metadata couldn't be used: " + ex.Message);
                        }
                    }

                    if (display != null)
                        parameter.Description = display;
                     if(display == null && !goOnline)
                        display = (pname == null) ? localKey : pname;

                    if (units != null)
                        parameter.Units = units;
                    if (palette != null)
                        parameter.PaletteDescription = palette.Value;
                    //if (verticalCoord != "") TODO: for now there is not enough of metadata in a file!
                        // parameter.VerticalCoord = new VerticalCoordDescription(); 

                    parameters.Add(new EsseResponseParameter(parameter, this));
                }
            }
        }

        public Stream GetCompressedStream(EsseParameterDescription parameter)
        {
            XmlNode dataNode = response.SelectSingleNode("//types:result[@name='XmlDataCompressed" + parameter.EsseParameter.ToString() + "']", ns);
            if (dataNode == null)
                throw new Exception("Unknown parameter: XmlDataCompressed" + parameter.EsseParameter.ToString() + " element is not found");
            string dataString = ((XmlElement)dataNode).InnerText;
            return new MemoryStream(Convert.FromBase64String(dataString));
        }

        public Stream GetUncompressedStream(EsseParameterDescription parameter)
        {
            return new System.IO.Compression.GZipStream(GetCompressedStream(parameter), System.IO.Compression.CompressionMode.Decompress);
        }

        public XmlReader GetXmlReader(EsseParameterDescription parameter)
        {
            if (isOGSADAIWrapped)
            {
                Stream s = GetUncompressedStream(parameter);
                return XmlReader.Create(new System.IO.StreamReader(s));
            }

            ////////////////////////////////////////////////////
            // no OGSA-DAI wrap            
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(response.InnerXml);
            Stream stream = new MemoryStream(buffer);
            XmlReader reader = XmlReader.Create(stream);

            // TODO: make parameter search by its name
            return reader;
        }

        public XmlReader GetXmlReaderForData(EsseParameterDescription parameter, DateTime date)
        {
            EsseResponseParameter erp = parameters[0];
            TimeSpan dist = date - erp.Region.From;

            int index;
            if (erp.Region.TimeStep.TotalMinutes == 0) index = 0;
            else index = (int)(dist.TotalMinutes / erp.Region.TimeStep.TotalMinutes);

            XmlReader reader = GetXmlReader(parameter);

            //StreamWriter w = new StreamWriter(@"c:\temp\log.txt");
            //DateTime d = erp.Region.From;

            for (int i = 0; i <= index; i++)
            {
                //bool b = reader.ReadToFollowing("v");
                //if (!b)
                //{
                //    w.Close();
                //    Debug.WriteLine(string.Format("{0}: NOT found", i));
                //    w.WriteLine(string.Format("{0}: NOT found", i));                
                //}
                //reader.ReadToFollowing("t");
                //reader.Read();
                //string val = reader.Value;

                //Debug.WriteLine(string.Format("{0}: {1}: found", i, val));
                //w.WriteLine(string.Format("{0}: {1} ({2}): found", i, val, d));
                //d = d.Add(erp.Region.TimeStep);

                if (!reader.ReadToFollowing("v"))
                    throw new Exception("Wrong response: data values are not found!");
            }

            //w.Close();
            return reader;
        }

        public static DateTime ParseDateTimeWithSuffix(string dateString)
        {
            return DateTime.Parse(dateString.Substring(0, 19));
            //try
            //{
            //    if (dateString.EndsWith("GMT") || dateString.EndsWith("UTC"))
            //    {
            //        dateString = dateString.Remove(dateString.Length - 3) + "Z";
            //        return DateTime.Parse(dateString, System.Globalization.CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal);
            //    }
            //    else
            //        return DateTime.Parse(dateString);
            //}
            //catch (Exception exc)
            //{
            //    System.Windows.Forms.MessageBox.Show(dateString + ": " + exc.Message);
            //    throw exc;
            //}
        }

        public EsseRegion GetRegion(EsseParameterDescription parameter)
        {
            XmlReader reader = GetXmlReader(parameter);
            if (isOGSADAIWrapped) // TODO: <range> must be removed from wrapped data too?
            {
                try
                {
                    EsseRegion region = new EsseRegion();
                    bool timeRangeFound = false;
                    while (reader.ReadToFollowing("range"))
                    {
                        reader.MoveToAttribute("name");
                        if (!reader.ReadAttributeValue())
                            continue;
                        if (reader.Value == "time")
                        {
                            reader.MoveToAttribute("min");
                            if (!reader.ReadAttributeValue())
                                throw new Exception("Unable to parse file: min attribute not found");
                            region.From = ParseDateTimeWithSuffix(reader.Value);
                            reader.MoveToAttribute("max");
                            if (!reader.ReadAttributeValue())
                                throw new Exception("Unable to parse file: max attribute not found");
                            region.To = ParseDateTimeWithSuffix(reader.Value);
                            reader.MoveToAttribute("step");
                            if (!reader.ReadAttributeValue())
                                throw new Exception("Unable to parse file: step attribute not found");
                            int stepMinutes = Int32.Parse(reader.Value);
                            region.TimeStep = new TimeSpan(0, stepMinutes, 0);
                            timeRangeFound = true;
                            break;
                        }
                    }
                    if (!timeRangeFound)
                        throw new Exception("No time range is found");
                    if (!reader.ReadToFollowing("grid"))
                        throw new Exception("No grid is found in response");
                    if (!reader.ReadToDescendant("point"))
                        throw new Exception("Wrong response: grid is empty");
                    do
                    {
                        string point = reader.ReadString();
                        string[] coords = point.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                        region.AddPoint(new double2(Double.Parse(coords[1], CultureInfo.InvariantCulture),
                                                    Double.Parse(coords[0], CultureInfo.InvariantCulture)));
                    } while (reader.ReadToNextSibling("point"));
                    return region;
                }
                finally
                {
                    reader.Close();
                }
            }
            else // no OGSA-DAI wrap
            {
                try
                {
                    EsseRegion region = new EsseRegion();

                    ////////////////////////////////////////////////////////////////
                    // Reading grid
                    if (!reader.ReadToFollowing("grid"))
                        throw new Exception("No grid is found in response");
                    if (!reader.ReadToDescendant("point"))
                        throw new Exception("Wrong response: grid is empty");
                    do
                    {
                        string point = reader.ReadString();
                        string[] coords = point.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                        region.AddPoint(new double2(Double.Parse(coords[1], CultureInfo.InvariantCulture),
                                                    Double.Parse(coords[0], CultureInfo.InvariantCulture)));
                    } while (reader.ReadToNextSibling("point"));

                    /////////////////////////////////////////////////////////////////
                    // Reading time-range from <t> elements
                    DateTime min = DateTime.MaxValue, max = DateTime.MinValue;
                    int count = 0;
                    region.TimeStep = new TimeSpan(0);
                    while (reader.ReadToFollowing("t"))
                    {
                        DateTime current = ParseDateTimeWithSuffix(reader.ReadElementContentAsString());
                        if (current < min) min = current;
                        if (current > max) max = current;

                        if (count++ == 1)
                        {
                            region.TimeStep = current - min;
                        }
                    }
                    if (count == 0) throw new Exception("No time span found in response");

                    region.From = min;
                    region.To = max;
                    //region.TimeStep = count == 1 ? 
                    //    new TimeSpan(0) :
                    //    TimeSpan.FromMilliseconds(((TimeSpan)(max - min)).TotalMilliseconds / (count - 1));

                    return region;
                }
                finally
                {
                    reader.Close();
                }

            }
        }
    }
}